08 — 几何体

Three.js Journey · Bruno Simon · 难度:简单

08 — 几何体 课程海报

介绍

到目前为止,我们只使用了 BoxGeometry 类来创建立方体。在这节课中,我们会认识各种其他几何体,但首先,我们需要理解几何体到底是什么。

什么是几何体?

在 Three.js 中,几何体由顶点(3D 空间中的坐标点)和面(把这些顶点连接起来形成表面的三角形)组成。

我们用几何体来创建 mesh,但你也可以把几何体用于生成粒子。每个顶点(vertices 的单数形式)会对应一个粒子,不过这是以后课程的内容。

我们可以在顶点里存储比位置更多的数据。一个很好的例子是 UV 坐标或法线(normals)。你会看到,我们以后会学到更多这些内容。

各种内置几何体

Three.js 有许多内置几何体。虽然你不需要精确地知道如何实例化每一个,但知道它们的存在是件好事。

我们要看到的所有内置几何体都继承自 BufferGeometry 类。这个类有很多内置方法,比如 translate(...)、rotateX(...)、normalize() 等等,但本课中我们不会用到它们。

大多数几何体的文档页面都带有示例。

如果你需要一个 Three.js 不支持的特殊几何体,你可以用 JavaScript 创建自己的几何体,或者在 3D 软件里制作,导出后导入项目。我们以后会学到更多这些内容。

Box 示例

我们已经做过一个立方体,但我们没有多聊它的参数。大多数几何体都有参数,使用前你总该先看看文档。

BoxGeometry 有 6 个参数:

细分对应的是每个面应该由多少个三角形组成。默认是 1,意思是每个面只有 2 个三角形。如果你把细分设为 2,每个面就会变成 8 个三角形:

const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2)

问题是我们看不到这些三角形。

一个好办法是给我们的材质添加 wireframe: true。线框会显示分隔每个三角形的线条:

const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })

如你所见,每个面有 8 个三角形。

虽然这对一个平面立方体没什么看点,但用于 SphereGeometry 时就更有意思了:

const geometry = new THREE.SphereGeometry(1, 32, 32)

我们加的细分越多,就越难以分辨各个面。但记住,顶点和面太多会影响性能。

创建你自己的 BufferGeometry

有时候,我们需要创建自己的几何体。如果几何体非常复杂或形状很精确,最好在 3D 软件中创建它(这我们会留到以后的课程讲),但如果几何体不算太复杂,我们也可以自己用 BufferGeometry 来构建。

要创建你自己的 BufferGeometry,先实例化一个空的 BufferGeometry。我们要创建一个简单的三角形:

// Create an empty BufferGeometry
const geometry = new THREE.BufferGeometry()

要向 BufferGeometry 添加顶点,你必须先有一个 Float32Array。

Float32Array 是原生 JavaScript 的 typed array(类型化数组)。里面只能存放浮点数,而且数组长度是固定的。

要创建 Float32Array,你可以指定它的长度,以后再填充:

const positionsArray = new Float32Array(9)

// First vertice
positionsArray[0] = 0
positionsArray[1] = 0
positionsArray[2] = 0

// Second vertice
positionsArray[3] = 0
positionsArray[4] = 1
positionsArray[5] = 0

// Third vertice
positionsArray[6] = 1
positionsArray[7] = 0
positionsArray[8] = 0

或者你可以直接传一个数组:

const positionsArray = new Float32Array([
    0, 0, 0, // First vertex
    0, 1, 0, // Second vertex
    1, 0, 0  // Third vertex
])

如你所见,顶点的坐标是按顺序线性指定的。这是一个一维数组,你先指定第一个顶点的 x、y、z,然后第二个顶点的 x、y、z,依此类推。

在把这个数组发送给 BufferGeometry 之前,你必须先把它转换成 BufferAttribute。

第一个参数对应你的类型化数组,第二个参数对应构成一个顶点属性需要多少个值。正如我们前面看到的,要读取这个数组,我们必须每 3 个一组,因为一个顶点位置由 3 个值(x、y、z)组成:

const positionsAttribute = new THREE.BufferAttribute(positionsArray, 3)

然后我们可以用 setAttribute(...) 方法把这个属性添加到我们的 BufferGeometry。第一个参数是属性的名称,第二个参数是它的值:

geometry.setAttribute('position', positionsAttribute)

我们选择 'position' 作为名称,因为 Three.js 内部的 shader 会查找这个值来定位顶点。我们在 shader 课程里会讲到更多这些内容。

面会按照顶点的顺序自动创建。

全部合在一起:

// Create an empty BufferGeometry
const geometry = new THREE.BufferGeometry()

// Create a Float32Array containing the vertices position (3 by 3)
const positionsArray = new Float32Array([
    0, 0, 0, // First vertex
    0, 1, 0, // Second vertex
    1, 0, 0  // Third vertex
])

// Create the attribute and name it 'position'
const positionsAttribute = new THREE.BufferAttribute(positionsArray, 3)
geometry.setAttribute('position', positionsAttribute)

我们还可以创建一堆随机三角形:

// Create an empty BufferGeometry
const geometry = new THREE.BufferGeometry()

// Create 50 triangles (450 values)
const count = 50
const positionsArray = new Float32Array(count * 3 * 3)
for(let i = 0; i < count * 3 * 3; i++)
{
    positionsArray[i] = (Math.random() - 0.5) * 4
}

// Create the attribute and name it 'position'
const positionsAttribute = new THREE.BufferAttribute(positionsArray, 3)
geometry.setAttribute('position', positionsAttribute)

唯一的难点可能是 count * 3 * 3 这部分,但解释起来很简单:我们需要 50 个三角形。每个三角形由 3 个顶点组成,每个顶点由 3 个值(x、y、z)组成。

索引

BufferGeometry 有一个有趣的地方,就是你可以用 index 属性复用顶点。以立方体为例,多个面可以共用某些顶点,比如角落的那些顶点。再仔细看,每个顶点都可以被多个相邻三角形使用。这会得到一个更小的属性数组和性能提升。但我们这节课不会覆盖这部分内容。